'ChangesListSpecialPageQuery': Called when building SQL query on pages
inheriting from ChangesListSpecialPage (in core: RecentChanges,
RecentChangesLinked and Watchlist).
-
Do not use this to implement individual filters if they are compatible with the
ChangesListFilter and ChangesListFilterGroup structure.
-
Instead, use sub-classes of those classes, in conjunction with the
ChangesListSpecialPageStructuredFilters hook.
-
This hook can be used to implement filters that do not implement that structure,
or custom behavior that is not an individual filter.
$name: name of the special page, e.g. 'Watchlist'
filters for pages inheriting from ChangesListSpecialPage (in core: RecentChanges,
RecentChangesLinked, and Watchlist). Generally, you will want to construct
new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects.
-
When constructing them, you specify which group they belong to. You can reuse
existing groups (accessed through $special->getFilterGroup), or create your own
(ChangesListBooleanFilterGroup or ChangesListStringOptionsFilterGroup).
If you create new groups, you must register them with $special->registerFilterGroup.
-
Note that this is called regardless of whether the user is currently using
the new (structured) or old (unstructured) filter UI. If you want your boolean
filter to show on both the new and old UI, specify all the supported fields.
These include showHide, label, and description.
-
See the constructor of each ChangesList* class for documentation of supported
fields.
-
$special: ChangesListSpecialPage instance
'ChangeTagAfterDelete': Called after a change tag has been deleted (that is,
static $results = [];
if ( $prefix == '' ) {
- return $fields;
+ return array_merge( $fields, [ 'description' ] );
}
-
if ( !isset( $results[$prefix] ) ) {
$prefixedFields = [];
foreach ( $fields as $field ) {
// 1.25
// note this patch covers other _comment and _description fields too
- [ 'modifyField', 'recentchanges', 'rc_comment', 'patch-editsummary-length.sql' ],
+ [ 'doExtendCommentLengths' ],
// 1.26
[ 'dropTable', 'hitcounter' ],
);
}
+ protected function doExtendCommentLengths() {
+ $table = $this->db->tableName( 'revision' );
+ $res = $this->db->query( "SHOW COLUMNS FROM $table LIKE 'rev_comment'" );
+ $row = $this->db->fetchObject( $res );
+
+ if ( $row && ( $row->Type !== "varbinary(767)" || $row->Default !== "" ) ) {
+ $this->applyPatch(
+ 'patch-editsummary-length.sql',
+ false,
+ 'Extending edit summary lengths (and setting defaults)'
+ );
+ } else {
+ $this->output( '...comment fields are up to date' );
+ }
+ }
+
public function getSchemaVars() {
global $wgDBTableOptions;
-ALTER TABLE /*_*/revision MODIFY rev_comment varbinary(767) NOT NULL;
-ALTER TABLE /*_*/archive MODIFY ar_comment varbinary(767) NOT NULL;
-ALTER TABLE /*_*/image MODIFY img_description varbinary(767) NOT NULL;
-ALTER TABLE /*_*/oldimage MODIFY oi_description varbinary(767) NOT NULL;
-ALTER TABLE /*_*/filearchive MODIFY fa_description varbinary(767);
+ALTER TABLE /*_*/revision MODIFY rev_comment varbinary(767) NOT NULL default '';
+ALTER TABLE /*_*/archive MODIFY ar_comment varbinary(767) NOT NULL default '';
+ALTER TABLE /*_*/image MODIFY img_description varbinary(767) NOT NULL default '';
+ALTER TABLE /*_*/oldimage MODIFY oi_description varbinary(767) NOT NULL default '';
+ALTER TABLE /*_*/filearchive MODIFY fa_description varbinary(767) default '';
ALTER TABLE /*_*/filearchive MODIFY fa_deleted_reason varbinary(767) default '';
ALTER TABLE /*_*/recentchanges MODIFY rc_comment varbinary(767) NOT NULL default '';
ALTER TABLE /*_*/logging MODIFY log_comment varbinary(767) NOT NULL default '';
-ALTER TABLE /*_*/ipblocks MODIFY ipb_reason varbinary(767) NOT NULL;
-ALTER TABLE /*_*/protected_titles MODIFY pt_reason varbinary(767);
+ALTER TABLE /*_*/ipblocks MODIFY ipb_reason varbinary(767) NOT NULL default '';
+ALTER TABLE /*_*/protected_titles MODIFY pt_reason varbinary(767) default '';
$nonRecurseDirs = [
"$IP/",
];
+ $extraFiles = [
+ "$IP/tests/phpunit/MediaWikiTestCase.php",
+ ];
foreach ( $recurseDirs as $dir ) {
$ret = $this->getHooksFromDir( $dir, self::FIND_RECURSIVE );
$potentialHooks = array_merge( $potentialHooks, $ret['good'] );
$badHooks = array_merge( $badHooks, $ret['bad'] );
}
+ foreach ( $extraFiles as $file ) {
+ $potentialHooks = array_merge( $potentialHooks, $this->getHooksFromFile( $file ) );
+ $badHooks = array_merge( $badHooks, $this->getBadHooksFromFile( $file ) );
+ }
$documented = array_keys( $documentedHooks );
$potential = array_keys( $potentialHooks );
$n = [];
if ( preg_match_all( '/((?:[^,\(\)]|\([^\(\)]*\))+)/', $match[4], $n ) ) {
$args = array_map( 'trim', $n[1] );
+ // remove empty entries from trailing spaces
+ $args = array_filter( $args );
}
} elseif ( isset( $match[3] ) ) {
// Found a parameter for Hooks::run,
TEXT
);
$this->addOption( 'rev-id', 'The rev_id to start copying from. Default: 0', false, true );
+ $this->addOption(
+ 'max-rev-id',
+ 'The rev_id to stop at. Default: result of MAX(rev_id)',
+ false,
+ true
+ );
$this->addOption(
'throttle',
'Wait this many milliseconds after copying each batch of revisions. Default: 0',
public function doDBUpdates() {
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $dbr = $this->getDB( DB_REPLICA, [ 'vslow' ] );
$dbw = $this->getDB( DB_MASTER );
$throttle = intval( $this->getOption( 'throttle', 0 ) );
+ $maxRevId = intval( $this->getOption( 'max-rev-id', 0 ) );
$start = $this->getOption( 'rev-id', 0 );
- $end = $dbw->selectField( 'revision', 'MAX(rev_id)', false, __METHOD__ );
+ $end = $maxRevId > 0
+ ? $maxRevId
+ : $dbw->selectField( 'revision', 'MAX(rev_id)', false, __METHOD__ );
$blockStart = $start;
$revCount = 0;
$this->output( "Copying IP revisions to ip_changes, from rev_id $start to rev_id $end\n" );
while ( $blockStart <= $end ) {
- $rows = $dbw->select(
+ $blockEnd = min( $blockStart + 200, $end );
+ $rows = $dbr->select(
'revision',
[ 'rev_id', 'rev_timestamp', 'rev_user_text' ],
- [ "rev_id >= $blockStart", 'rev_user' => 0 ],
+ [ "rev_id BETWEEN $blockStart AND $blockEnd", 'rev_user' => 0 ],
__METHOD__,
[ 'ORDER BY' => 'rev_id ASC', 'LIMIT' => $this->mBatchSize ]
);
}
$this->output( "...checking $this->mBatchSize revisions for IP edits that need copying, " .
- "starting with rev_id $blockStart\n" );
+ "between rev_ids $blockStart and $blockEnd\n" );
$insertRows = [];
foreach ( $rows as $row ) {
'styles' => 'resources/src/mediawiki.special/mediawiki.special.pagesWithProp.css',
],
'mediawiki.special.preferences' => [
- 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.preferences.js',
+ 'scripts' => [
+ 'resources/src/mediawiki.special/mediawiki.special.preferences.confirmClose.js',
+ 'resources/src/mediawiki.special/mediawiki.special.preferences.convertmessagebox.js',
+ 'resources/src/mediawiki.special/mediawiki.special.preferences.tabs.js',
+ 'resources/src/mediawiki.special/mediawiki.special.preferences.timezone.js',
+ ],
'messages' => [
'prefs-tabs-navigation-hint',
'prefswarning-warning',
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Enable save button and prevent the window being accidentally
+ * closed when any form field is changed.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var allowCloseWindow;
+
+ // Check if all of the form values are unchanged
+ function isPrefsChanged() {
+ var inputs = $( '#mw-prefs-form :input[name]' ),
+ input, $input, inputType,
+ index, optIndex,
+ opt;
+
+ for ( index = 0; index < inputs.length; index++ ) {
+ input = inputs[ index ];
+ $input = $( input );
+
+ // Different types of inputs have different methods for accessing defaults
+ if ( $input.is( 'select' ) ) {
+ // <select> has the property defaultSelected for each option
+ for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
+ opt = input.options[ optIndex ];
+ if ( opt.selected !== opt.defaultSelected ) {
+ return true;
+ }
+ }
+ } else if ( $input.is( 'input' ) ) { // <input> has defaultValue or defaultChecked
+ inputType = input.type;
+ if ( inputType === 'radio' || inputType === 'checkbox' ) {
+ if ( input.checked !== input.defaultChecked ) {
+ return true;
+ }
+ } else if ( input.value !== input.defaultValue ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // Disable the button to save preferences unless preferences have changed
+ // Check if preferences have been changed before JS has finished loading
+ if ( !isPrefsChanged() ) {
+ $( '#prefcontrol' ).prop( 'disabled', true );
+ $( '#preferences > fieldset' ).one( 'change keydown mousedown', function () {
+ $( '#prefcontrol' ).prop( 'disabled', false );
+ } );
+ }
+
+ // Set up a message to notify users if they try to leave the page without
+ // saving.
+ allowCloseWindow = mw.confirmCloseWindow( {
+ test: isPrefsChanged,
+ message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
+ namespace: 'prefswarning'
+ } );
+ $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) );
+ $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) );
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Check for successbox to replace with notifications.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
+ convertmessagebox();
+ } );
+}( mediaWiki, jQuery ) );
+++ /dev/null
-/*!
- * JavaScript for Special:Preferences
- */
-( function ( mw, $ ) {
- $( function () {
- var $preftoc, $preferences, $fieldsets, labelFunc, previousTab,
- $tzSelect, $tzTextbox, $localtimeHolder, servertime, allowCloseWindow,
- convertmessagebox = require( 'mediawiki.notification.convertmessagebox' );
-
- labelFunc = function () {
- return this.id.replace( /^mw-prefsection/g, 'preftab' );
- };
-
- $preftoc = $( '#preftoc' );
- $preferences = $( '#preferences' );
-
- $fieldsets = $preferences.children( 'fieldset' )
- .attr( {
- role: 'tabpanel',
- 'aria-labelledby': labelFunc
- } );
- $fieldsets.not( '#mw-prefsection-personal' )
- .hide()
- .attr( 'aria-hidden', 'true' );
-
- // T115692: The following is kept for backwards compatibility with older skins
- $preferences.addClass( 'jsprefs' );
- $fieldsets.addClass( 'prefsection' );
- $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
-
- // Make sure the accessibility tip is selectable so that screen reader users take notice,
- // but hide it per default to reduce interface clutter. Also make sure it becomes visible
- // when selected. Similar to jquery.mw-jump
- $( '<div>' ).addClass( 'mw-navigation-hint' )
- .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
- .attr( 'tabIndex', 0 )
- .on( 'focus blur', function ( e ) {
- if ( e.type === 'blur' || e.type === 'focusout' ) {
- $( this ).css( 'height', '0' );
- } else {
- $( this ).css( 'height', 'auto' );
- }
- } ).insertBefore( $preftoc );
-
- /**
- * It uses document.getElementById for security reasons (HTML injections in $()).
- *
- * @ignore
- * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
- * @param {string} [mode] A hash will be set according to the current
- * open section. Set mode 'noHash' to surpress this.
- */
- function switchPrefTab( name, mode ) {
- var $tab, scrollTop;
- // Handle hash manually to prevent jumping,
- // therefore save and restore scrollTop to prevent jumping.
- scrollTop = $( window ).scrollTop();
- if ( mode !== 'noHash' ) {
- location.hash = '#mw-prefsection-' + name;
- }
- $( window ).scrollTop( scrollTop );
-
- $preftoc.find( 'li' ).removeClass( 'selected' )
- .find( 'a' ).attr( {
- tabIndex: -1,
- 'aria-selected': 'false'
- } );
-
- $tab = $( document.getElementById( 'preftab-' + name ) );
- if ( $tab.length ) {
- $tab.attr( {
- tabIndex: 0,
- 'aria-selected': 'true'
- } ).focus()
- .parent().addClass( 'selected' );
-
- $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
- $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
- }
- }
-
- // Check for successbox to replace with notifications
- convertmessagebox();
-
- // Enable keyboard users to use left and right keys to switch tabs
- $preftoc.on( 'keydown', function ( event ) {
- var keyLeft = 37,
- keyRight = 39,
- $el;
-
- if ( event.keyCode === keyLeft ) {
- $el = $( '#preftoc li.selected' ).prev().find( 'a' );
- } else if ( event.keyCode === keyRight ) {
- $el = $( '#preftoc li.selected' ).next().find( 'a' );
- } else {
- return;
- }
- if ( $el.length > 0 ) {
- switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
- }
- } );
-
- // Jump to correct section as indicated by the hash.
- // This function is called onload and onhashchange.
- function detectHash() {
- var hash = location.hash,
- matchedElement, parentSection;
- if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
- } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
- matchedElement = document.getElementById( hash.slice( 1 ) );
- parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
- if ( parentSection.length ) {
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- // Switch to proper tab and scroll to selected item.
- switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
- matchedElement.scrollIntoView();
- }
- }
- }
-
- // In browsers that support the onhashchange event we will not bind click
- // handlers and instead let the browser do the default behavior (clicking the
- // <a href="#.."> will naturally set the hash, handled by onhashchange.
- // But other things that change the hash will also be caught (e.g. using
- // the Back and Forward browser navigation).
- // Note the special check for IE "compatibility" mode.
- if ( 'onhashchange' in window &&
- ( document.documentMode === undefined || document.documentMode >= 8 )
- ) {
- $( window ).on( 'hashchange', function () {
- var hash = location.hash;
- if ( hash.match( /^#mw-[\w-]+/ ) ) {
- detectHash();
- } else if ( hash === '' ) {
- switchPrefTab( 'personal', 'noHash' );
- }
- } )
- // Run the function immediately to select the proper tab on startup.
- .trigger( 'hashchange' );
- // In older browsers we'll bind a click handler as fallback.
- // We must not have onhashchange *and* the click handlers, otherwise
- // the click handler calls switchPrefTab() which sets the hash value,
- // which triggers onhashchange and calls switchPrefTab() again.
- } else {
- $preftoc.on( 'click', 'li a', function ( e ) {
- switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
- e.preventDefault();
- } );
- // If we've reloaded the page or followed an open-in-new-window,
- // make the selected tab visible.
- detectHash();
- }
-
- // Timezone functions.
- // Guesses Timezone from browser and updates fields onchange.
-
- $tzSelect = $( '#mw-input-wptimecorrection' );
- $tzTextbox = $( '#mw-input-wptimecorrection-other' );
- $localtimeHolder = $( '#wpLocalTime' );
- servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
-
- function minutesToHours( min ) {
- var tzHour = Math.floor( Math.abs( min ) / 60 ),
- tzMin = Math.abs( min ) % 60,
- tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
- ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
- return tzString;
- }
-
- function hoursToMinutes( hour ) {
- var minutes,
- arr = hour.split( ':' );
-
- arr[ 0 ] = parseInt( arr[ 0 ], 10 );
-
- if ( arr.length === 1 ) {
- // Specification is of the form [-]XX
- minutes = arr[ 0 ] * 60;
- } else {
- // Specification is of the form [-]XX:XX
- minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
- if ( arr[ 0 ] < 0 ) {
- minutes *= -1;
- }
- }
- // Gracefully handle non-numbers.
- if ( isNaN( minutes ) ) {
- return 0;
- } else {
- return minutes;
- }
- }
-
- function updateTimezoneSelection() {
- var minuteDiff, localTime,
- type = $tzSelect.val();
-
- if ( type === 'other' ) {
- // User specified time zone manually in <input>
- // Grab data from the textbox, parse it.
- minuteDiff = hoursToMinutes( $tzTextbox.val() );
- } else {
- // Time zone not manually specified by user
- if ( type === 'guess' ) {
- // Get browser timezone & fill it in
- minuteDiff = -( new Date().getTimezoneOffset() );
- $tzTextbox.val( minutesToHours( minuteDiff ) );
- $tzSelect.val( 'other' );
- $tzTextbox.prop( 'disabled', false );
- } else {
- // Grab data from the $tzSelect value
- minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
- $tzTextbox.val( minutesToHours( minuteDiff ) );
- }
-
- // Set defaultValue prop on the generated box so we don't trigger the
- // unsaved preferences check
- $tzTextbox.prop( 'defaultValue', $tzTextbox.val() );
- }
-
- // Determine local time from server time and minutes difference, for display.
- localTime = servertime + minuteDiff;
-
- // Bring time within the [0,1440) range.
- localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
-
- $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
- }
-
- if ( $tzSelect.length && $tzTextbox.length ) {
- $tzSelect.change( updateTimezoneSelection );
- $tzTextbox.blur( updateTimezoneSelection );
- updateTimezoneSelection();
- }
-
- // Restore the active tab after saving the preferences
- previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
- if ( previousTab ) {
- switchPrefTab( previousTab, 'noHash' );
- // Deleting the key, the tab states should be reset until we press Save
- mw.storage.session.remove( 'mwpreferences-prevTab' );
- }
-
- $( '#mw-prefs-form' ).on( 'submit', function () {
- var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
- mw.storage.session.set( 'mwpreferences-prevTab', value );
- } );
-
- // Check if all of the form values are unchanged
- function isPrefsChanged() {
- var inputs = $( '#mw-prefs-form :input[name]' ),
- input, $input, inputType,
- index, optIndex,
- opt;
-
- for ( index = 0; index < inputs.length; index++ ) {
- input = inputs[ index ];
- $input = $( input );
-
- // Different types of inputs have different methods for accessing defaults
- if ( $input.is( 'select' ) ) {
- // <select> has the property defaultSelected for each option
- for ( optIndex = 0; optIndex < input.options.length; optIndex++ ) {
- opt = input.options[ optIndex ];
- if ( opt.selected !== opt.defaultSelected ) {
- return true;
- }
- }
- } else if ( $input.is( 'input' ) ) { // <input> has defaultValue or defaultChecked
- inputType = input.type;
- if ( inputType === 'radio' || inputType === 'checkbox' ) {
- if ( input.checked !== input.defaultChecked ) {
- return true;
- }
- } else if ( input.value !== input.defaultValue ) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- // Disable the button to save preferences unless preferences have changed
- // Check if preferences have been changed before JS has finished loading
- if ( !isPrefsChanged() ) {
- $( '#prefcontrol' ).prop( 'disabled', true );
- $( '#preferences > fieldset' ).one( 'change keydown mousedown', function () {
- $( '#prefcontrol' ).prop( 'disabled', false );
- } );
- }
-
- // Set up a message to notify users if they try to leave the page without
- // saving.
- allowCloseWindow = mw.confirmCloseWindow( {
- test: isPrefsChanged,
- message: mw.msg( 'prefswarning-warning', mw.msg( 'saveprefs' ) ),
- namespace: 'prefswarning'
- } );
- $( '#mw-prefs-form' ).submit( $.proxy( allowCloseWindow, 'release' ) );
- $( '#mw-prefs-restoreprefs' ).click( $.proxy( allowCloseWindow, 'release' ) );
- } );
-}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Tab navigation.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $preftoc, $preferences, $fieldsets, labelFunc, previousTab;
+
+ labelFunc = function () {
+ return this.id.replace( /^mw-prefsection/g, 'preftab' );
+ };
+
+ $preftoc = $( '#preftoc' );
+ $preferences = $( '#preferences' );
+
+ $fieldsets = $preferences.children( 'fieldset' )
+ .attr( {
+ role: 'tabpanel',
+ 'aria-labelledby': labelFunc
+ } );
+ $fieldsets.not( '#mw-prefsection-personal' )
+ .hide()
+ .attr( 'aria-hidden', 'true' );
+
+ // T115692: The following is kept for backwards compatibility with older skins
+ $preferences.addClass( 'jsprefs' );
+ $fieldsets.addClass( 'prefsection' );
+ $fieldsets.children( 'legend' ).addClass( 'mainLegend' );
+
+ // Make sure the accessibility tip is selectable so that screen reader users take notice,
+ // but hide it per default to reduce interface clutter. Also make sure it becomes visible
+ // when selected. Similar to jquery.mw-jump
+ $( '<div>' ).addClass( 'mw-navigation-hint' )
+ .text( mw.msg( 'prefs-tabs-navigation-hint' ) )
+ .attr( 'tabIndex', 0 )
+ .on( 'focus blur', function ( e ) {
+ if ( e.type === 'blur' || e.type === 'focusout' ) {
+ $( this ).css( 'height', '0' );
+ } else {
+ $( this ).css( 'height', 'auto' );
+ }
+ } ).insertBefore( $preftoc );
+
+ /**
+ * It uses document.getElementById for security reasons (HTML injections in $()).
+ *
+ * @ignore
+ * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+ * @param {string} [mode] A hash will be set according to the current
+ * open section. Set mode 'noHash' to surpress this.
+ */
+ function switchPrefTab( name, mode ) {
+ var $tab, scrollTop;
+ // Handle hash manually to prevent jumping,
+ // therefore save and restore scrollTop to prevent jumping.
+ scrollTop = $( window ).scrollTop();
+ if ( mode !== 'noHash' ) {
+ location.hash = '#mw-prefsection-' + name;
+ }
+ $( window ).scrollTop( scrollTop );
+
+ $preftoc.find( 'li' ).removeClass( 'selected' )
+ .find( 'a' ).attr( {
+ tabIndex: -1,
+ 'aria-selected': 'false'
+ } );
+
+ $tab = $( document.getElementById( 'preftab-' + name ) );
+ if ( $tab.length ) {
+ $tab.attr( {
+ tabIndex: 0,
+ 'aria-selected': 'true'
+ } ).focus()
+ .parent().addClass( 'selected' );
+
+ $preferences.children( 'fieldset' ).hide().attr( 'aria-hidden', 'true' );
+ $( document.getElementById( 'mw-prefsection-' + name ) ).show().attr( 'aria-hidden', 'false' );
+ }
+ }
+
+ // Enable keyboard users to use left and right keys to switch tabs
+ $preftoc.on( 'keydown', function ( event ) {
+ var keyLeft = 37,
+ keyRight = 39,
+ $el;
+
+ if ( event.keyCode === keyLeft ) {
+ $el = $( '#preftoc li.selected' ).prev().find( 'a' );
+ } else if ( event.keyCode === keyRight ) {
+ $el = $( '#preftoc li.selected' ).next().find( 'a' );
+ } else {
+ return;
+ }
+ if ( $el.length > 0 ) {
+ switchPrefTab( $el.attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+ }
+ } );
+
+ // Jump to correct section as indicated by the hash.
+ // This function is called onload and onhashchange.
+ function detectHash() {
+ var hash = location.hash,
+ matchedElement, parentSection;
+ if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+ } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
+ matchedElement = document.getElementById( hash.slice( 1 ) );
+ parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
+ if ( parentSection.length ) {
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ // Switch to proper tab and scroll to selected item.
+ switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), 'noHash' );
+ matchedElement.scrollIntoView();
+ }
+ }
+ }
+
+ // In browsers that support the onhashchange event we will not bind click
+ // handlers and instead let the browser do the default behavior (clicking the
+ // <a href="#.."> will naturally set the hash, handled by onhashchange.
+ // But other things that change the hash will also be caught (e.g. using
+ // the Back and Forward browser navigation).
+ // Note the special check for IE "compatibility" mode.
+ if ( 'onhashchange' in window &&
+ ( document.documentMode === undefined || document.documentMode >= 8 )
+ ) {
+ $( window ).on( 'hashchange', function () {
+ var hash = location.hash;
+ if ( hash.match( /^#mw-[\w-]+/ ) ) {
+ detectHash();
+ } else if ( hash === '' ) {
+ switchPrefTab( 'personal', 'noHash' );
+ }
+ } )
+ // Run the function immediately to select the proper tab on startup.
+ .trigger( 'hashchange' );
+ // In older browsers we'll bind a click handler as fallback.
+ // We must not have onhashchange *and* the click handlers, otherwise
+ // the click handler calls switchPrefTab() which sets the hash value,
+ // which triggers onhashchange and calls switchPrefTab() again.
+ } else {
+ $preftoc.on( 'click', 'li a', function ( e ) {
+ switchPrefTab( $( this ).attr( 'href' ).replace( '#mw-prefsection-', '' ) );
+ e.preventDefault();
+ } );
+ // If we've reloaded the page or followed an open-in-new-window,
+ // make the selected tab visible.
+ detectHash();
+ }
+
+ // Restore the active tab after saving the preferences
+ previousTab = mw.storage.session.get( 'mwpreferences-prevTab' );
+ if ( previousTab ) {
+ switchPrefTab( previousTab, 'noHash' );
+ // Deleting the key, the tab states should be reset until we press Save
+ mw.storage.session.remove( 'mwpreferences-prevTab' );
+ }
+
+ $( '#mw-prefs-form' ).on( 'submit', function () {
+ var value = $( $preftoc ).find( 'li.selected a' ).attr( 'id' ).replace( 'preftab-', '' );
+ mw.storage.session.set( 'mwpreferences-prevTab', value );
+ } );
+
+ } );
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*!
+ * JavaScript for Special:Preferences: Timezone field enhancements.
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var
+ $tzSelect, $tzTextbox, $localtimeHolder, servertime;
+
+ // Timezone functions.
+ // Guesses Timezone from browser and updates fields onchange.
+
+ $tzSelect = $( '#mw-input-wptimecorrection' );
+ $tzTextbox = $( '#mw-input-wptimecorrection-other' );
+ $localtimeHolder = $( '#wpLocalTime' );
+ servertime = parseInt( $( 'input[name="wpServerTime"]' ).val(), 10 );
+
+ function minutesToHours( min ) {
+ var tzHour = Math.floor( Math.abs( min ) / 60 ),
+ tzMin = Math.abs( min ) % 60,
+ tzString = ( ( min >= 0 ) ? '' : '-' ) + ( ( tzHour < 10 ) ? '0' : '' ) + tzHour +
+ ':' + ( ( tzMin < 10 ) ? '0' : '' ) + tzMin;
+ return tzString;
+ }
+
+ function hoursToMinutes( hour ) {
+ var minutes,
+ arr = hour.split( ':' );
+
+ arr[ 0 ] = parseInt( arr[ 0 ], 10 );
+
+ if ( arr.length === 1 ) {
+ // Specification is of the form [-]XX
+ minutes = arr[ 0 ] * 60;
+ } else {
+ // Specification is of the form [-]XX:XX
+ minutes = Math.abs( arr[ 0 ] ) * 60 + parseInt( arr[ 1 ], 10 );
+ if ( arr[ 0 ] < 0 ) {
+ minutes *= -1;
+ }
+ }
+ // Gracefully handle non-numbers.
+ if ( isNaN( minutes ) ) {
+ return 0;
+ } else {
+ return minutes;
+ }
+ }
+
+ function updateTimezoneSelection() {
+ var minuteDiff, localTime,
+ type = $tzSelect.val();
+
+ if ( type === 'other' ) {
+ // User specified time zone manually in <input>
+ // Grab data from the textbox, parse it.
+ minuteDiff = hoursToMinutes( $tzTextbox.val() );
+ } else {
+ // Time zone not manually specified by user
+ if ( type === 'guess' ) {
+ // Get browser timezone & fill it in
+ minuteDiff = -( new Date().getTimezoneOffset() );
+ $tzTextbox.val( minutesToHours( minuteDiff ) );
+ $tzSelect.val( 'other' );
+ $tzTextbox.prop( 'disabled', false );
+ } else {
+ // Grab data from the $tzSelect value
+ minuteDiff = parseInt( type.split( '|' )[ 1 ], 10 ) || 0;
+ $tzTextbox.val( minutesToHours( minuteDiff ) );
+ }
+
+ // Set defaultValue prop on the generated box so we don't trigger the
+ // unsaved preferences check
+ $tzTextbox.prop( 'defaultValue', $tzTextbox.val() );
+ }
+
+ // Determine local time from server time and minutes difference, for display.
+ localTime = servertime + minuteDiff;
+
+ // Bring time within the [0,1440) range.
+ localTime = ( ( localTime % 1440 ) + 1440 ) % 1440;
+
+ $localtimeHolder.text( mw.language.convertNumber( minutesToHours( localTime ) ) );
+ }
+
+ if ( $tzSelect.length && $tzTextbox.length ) {
+ $tzSelect.change( updateTimezoneSelection );
+ $tzTextbox.blur( updateTimezoneSelection );
+ updateTimezoneSelection();
+ }
+
+ } );
+}( mediaWiki, jQuery ) );
return Maintenance::DB_ADMIN;
}
+ protected function addOption( $name, $description, $required = false,
+ $withArg = false, $shortName = false, $multiOccurrence = false
+ ) {
+ // ignore --quiet which does not really make sense for unit tests
+ if ( $name !== 'quiet' ) {
+ parent::addOption( $name, $description, $required, $withArg, $shortName, $multiOccurrence );
+ }
+ }
+
/**
* Force the format of elements in $_SERVER['argv']
* - Split args such as "wiki=enwiki" into two separate arg elements "wiki" and "enwiki"